"""
Copyright © 2021 Walkline Wang (https://walkline.wang)
Gitee: https://gitee.com/walkline/micropython-qrcode-research
"""
import framebuf
from machine import I2C, Pin
from struct import pack
from drivers.ssd1306 import SSD1306_I2C
from qrcode_const import QRCodeConst


class QRCode(object):
	MIN_SIZE = 21
	MAX_SIZE = 177
	SIZE_STEP = 4

	ECL_L = '01'
	ECL_M = '00'
	ECL_Q = '11'
	ECL_H = '10'
	ECLS = [ECL_L, ECL_M, ECL_Q, ECL_H]

	def __init__(self, max_width=128, max_height=64, oled=None):
		self.total_bit_count = 0
		self.__max_width = max_width
		self.__max_height = max_height
		self.max_size = min(self.__max_width, self.__max_height)
		self.size = 0
		self.version = 0
		self.__oled = oled

		# if self.__max_size < QRCode.MIN_SIZE:
		# 	self.__max_size = QRCode.MIN_SIZE
		# elif self.__max_size > QRCode.MAX_SIZE:
		# 	self.__max_size = QRCode.MAX_SIZE

		self.max_version = self.__get_max_version()
		self.max_size = (self.max_version - 1) * QRCode.SIZE_STEP + QRCode.MIN_SIZE

		self.__buffer = bytearray(self.__max_width * self.__max_height // 8)
		self.__framebuffer = framebuf.FrameBuffer(self.__buffer, self.__max_width, self.__max_height, framebuf.MONO_VLSB)

		self.draw_qrcode()

	def generate(self, chars:str, ecl=None):
		if not ecl or ecl not in QRCode.ECLS:
			ecl = QRCode.ECL_M

		self.ecl = ecl
		self.data_format = self.__get_data_format(chars)
		
		if not self.data_format:
			print('not supportted char mode')
			return

		self.version = self.__get_version(chars)
		self.size = (self.version - 1) * QRCode.SIZE_STEP + QRCode.MIN_SIZE
		self.bit_length = self.__get_bit_length()

		if not self.bit_length:
			print('not supported bit length')
			return

		if self.data_format == QRCodeConst.DF_NUMERIC_MODE:
			self.__encoded_data = self.__encode_numeric(chars)
		elif self.data_format == QRCodeConst.DF_ALPHANUMERIC_MODE:
			pass
		elif self.data_format == QRCodeConst.DF_BYTE_MODE:
			pass
		elif self.data_format == QRCodeConst.DF_KANJI_MODE or\
			 self.data_format == QRCodeConst.DF_HANZI_MODE:
			pass

		print(self.__encoded_data)

	def __encode_numeric(self, chars:str):
		'''
		对数字类型的字符串进行编码
		'''
		step = 3
		segments = ''

		def zfill(value, length):
			'''
			如果 value 的二进制长度小于 length，则在二进制左侧补零直到二进制长度等于 length
			'''
			if isinstance(value, str):
				value = int(value)
			value = bin(value).lstrip('0b')
			if len(value) < length:
				value = '0' * (length - len(value)) + value
			return value

		segments += self.data_format # 添加 模式指示符
		segments += zfill(len(chars), self.bit_length) # 添加 字符串长度指示符

		for index in range(0, len(chars), step):
			segment = chars[index:min(index + 3, len(chars))].lstrip('0')

			if len(segment) == step:
				segments += zfill(segment, self.bit_length)
			elif len(segment) == 2:
				segments += zfill(segment, 7)
			elif len(segment) == 1:
				segments += zfill(segment, 4)
		# 0001 0000001010 1101111010 0001111011 0111001000 0111

		# 在编码字符结尾添加 结束标志
		if len(segments) < self.total_bit_count:
			if self.total_bit_count - len(segments) >= 4:
				segments += QRCodeConst.DF_TERMINATOR
			else:
				segments += '0' * (self.total_bit_count - len(segments))
		# 0001 0000001010 1101111010 0001111011 0111001000 0111 0000

		# 如果编码字符长度不是 8 的整倍数，则在结尾补零凑齐
		if len(segments) % 8 != 0:
			segments += '0' * (len(segments) % 8)
		# 0001 0000001010 1101111010 0001111011 0111001000 0111 0000 0000

		# 如果编码字符长度不够 codewords 长度，则在结尾循环添加 pad_bytes 直到凑齐
		if len(segments) < self.total_bit_count:
			pad_bytes = ['11101100', '00010001']
			for count in range((self.total_bit_count - len(segments)) // 8):
				segments += pad_bytes[count % 2]
		# 0001 0000001010 1101111010 0001111011 0111001000 0111 0000 0000 11101100 00010001 11101100 00010001 11101100 00010001 11101100 00010001 11101100

		return segments

	def __generate_error_correction_code(self, data):
		if self.version <= 2:
			return data

	def __get_bit_length(self):
		'''
		根据数据类型和版本号获取数据位长度，目前未考虑混合模式
		'''
		if 1 <= self.version <= 9:
			return QRCodeConst.DATA_FORMAT_BIT_LENGTH_MAP_1_TO_9.get(self.data_format)
		elif 10 <= self.version <= 26:
			return QRCodeConst.DATA_FORMAT_BIT_LENGTH_MAP_10_TO_26.get(self.data_format)
		elif 27 <= self.version <= 40:
			return QRCodeConst.DATA_FORMAT_BIT_LENGTH_MAP_27_TO_40.get(self.data_format)

		return 0

	def __get_data_format(self, chars:str):
		'''
		根据字符串内容判断数据类型，包括：数字、英文数字符号、二进制和日（中）文，目前未考虑混合模式
		'''
		if chars.isdigit():
			return QRCodeConst.DF_NUMERIC_MODE

		keys = QRCodeConst.ALPHANUMERIC_MODE_CHARSET_TABLE.keys()

		if all(key in keys for key in chars):
			return QRCodeConst.DF_ALPHANUMERIC_MODE
		else:
			return QRCodeConst.DF_BYTE_MODE

	def __get_max_version(self):
		'''
		根据指定的画布尺寸计算最大支持的版本号
		'''
		version = int((self.max_size - QRCode.MIN_SIZE) // QRCode.SIZE_STEP + 1)
		return version

	def __get_version(self, chars:str):
		'''
		根据字符串和指定的纠错级别计算实际版本号
		'''
		length = len(chars)

		for version, counts in QRCodeConst.ERROR_CORRECTION_LEVEL_MAP.get(self.ecl).items():
			if self.data_format == QRCodeConst.DF_NUMERIC_MODE and length <= counts[2]:
				self.total_bit_count = counts[1]
				return version
			elif self.data_format == QRCodeConst.DF_ALPHANUMERIC_MODE and length <= counts[3]:
				self.total_bit_count = counts[1]
				return version
			elif self.data_format == QRCodeConst.DF_BYTE_MODE and length <= counts[4]:
				self.total_bit_count = counts[1]
				return version
			elif (self.data_format == QRCodeConst.DF_KANJI_MODE or self.data_format == QRCodeConst.DF_HANZI_MODE) and length <= counts[5]:
				self.total_bit_count = counts[1]
				return version
			else:
				# complex
				pass

	def draw_qrcode(self):
		self.__draw_position_detection_pattern()
		self.__draw_timing_pattern()
		self.__draw_alignment_pattern()
		self.__draw_dark_module()

		if self.__oled:
			self.__oled.blit(self.__framebuffer, 0, 0)
			self.__oled.show()

	def __draw_format_information(self):
		pass

	def __draw_version_information(self):
		if self.max_version < 7: return

	def __draw_dark_module(self):
		self.__framebuffer.pixel(8, self.max_version * QRCode.SIZE_STEP + 9, 1)

	def __draw_timing_pattern(self):
		for pos_x in range(8, self.max_size - 8, 2):
			self.__framebuffer.pixel(pos_x, 6, 1)

		for pos_y in range(8,  self.max_size - 8, 2):
			self.__framebuffer.pixel(6, pos_y, 1)

	def __draw_alignment_pattern(self):
		self.alignment_count, *coordinates = QRCodeConst.ALIGNMENT_PATTERNS_MAP[self.max_version]

		for position in self.__combination(coordinates):
			pos_x, pos_y = position

			if pos_x - 2 < 7 and pos_y - 2 < 7: # 左上角已占用
				continue
			elif pos_x + 2 > self.max_size - 7 and pos_y - 2 < 7: # 右上角已占用
				continue
			elif pos_x - 2 < 7 and pos_y + 2 > self.max_size - 7: # 左下角已占用
				continue
		
			self.__framebuffer.pixel(pos_x, pos_y, 1)
			pos_x -= 2
			pos_y -= 2
			self.__framebuffer.rect(pos_x, pos_y, 5, 5, 1)

	def __draw_position_detection_pattern(self):
		for index in range(3):
			if index == 0: # 左上角
				pos_x = pos_y = 0
			elif index == 1: # 右上角
				pos_x = self.max_size - 7
				pos_y = 0
			else: # 左下角
				pos_x = 0
				pos_y = self.max_size - 7

			self.__framebuffer.rect(pos_x, pos_y, 7, 7, 1)
			pos_x += 2
			pos_y += 2
			self.__framebuffer.fill_rect(pos_x, pos_y, 3, 3, 1)

	def __combination(self, data):
		'''
		根据顶点信息生成坐标轴序列
		'''
		result = []

		for first in data:
			for second in data:
				result.append([first, second])

		return result

	def export_bmp(self, filename):
		identify = b'BM'
		reserved = 0
		header_size = const(62)
		bmp_header = const(40) # windows bitmap
		bmp_width = self.__max_width
		bmp_height = self.__max_height
		bmp_planes = 1
		bmp_bit_count = 1
		bmp_compression = 0
		bmp_data_size = int(bmp_width * bmp_height // 8)
		bmp_x_ppm = 0
		bmp_y_ppm = 0
		bmp_color_used = 0
		bmp_color_important = 0
		bmp_color_table_w = 0x00000000
		bmp_color_table_b = 0x00ffffff
		filesize = header_size + bmp_data_size

		bmp_header_data = pack(
			'<2s6I2H6I2I',
			# BITMAPFILEHEADER
			identify,			# 2
			filesize,			# 4
			reserved,			# 4
			header_size,		# 4

			# BITMAPINFOHEADER
			bmp_header,			# 4
			bmp_width,			# 4
			bmp_height * -1,	# 4
			bmp_planes,			# 2
			bmp_bit_count,		# 2
			bmp_compression,	# 4
			bmp_data_size,		# 4
			bmp_x_ppm,			# 4
			bmp_y_ppm,			# 4
			bmp_color_used,		# 4
			bmp_color_important,# 4

			# RGBQUAD
			bmp_color_table_w,	# 4
			bmp_color_table_b,	# 4
		)

		bmp_content_data = bytearray(bmp_data_size)
		converter = framebuf.FrameBuffer(bmp_content_data, self.__max_width, self.__max_height, framebuf.MONO_HLSB)
		converter.blit(self.__framebuffer, 0, 0)

		with open(f'{filename}.bmp', 'wb') as bmp:
			bmp.write(bmp_header_data)
			bmp.write(bmp_content_data)
		
		print(f'\n{filename}.bmp exported')

	def preview(self):
		print('\nPreview')

		for index in range(len(self.__buffer) // self.__max_width):
			byte_list = []

			for byte in self.__buffer[index * self.__max_width: index * self.__max_width + self.__max_width]:
				byte_list.append('{:08b}'.format(byte).replace('0', '.').replace('1', '@'))

			for bit in range(7, -1, -1):
				for row in byte_list[:self.max_size]:
					print(row[bit], end='')
				print('')


if __name__ == '__main__':
	oled = None
	i2c = I2C(0, scl=Pin(18), sda=Pin(19))
	slave_list = i2c.scan()

	if slave_list:
		oled = SSD1306_I2C(128, 64, i2c)

	test_numeric = '8901234567'
	qrcode = QRCode(128, 64, oled)
	qrcode.generate(test_numeric, QRCode.ECL_M)

	print('QR Code Info:')
	print(f'    max size: {qrcode.max_size}')
	print(f' max version: {qrcode.max_version}')
	print(f'        size: {qrcode.size}')
	print(f'     version: {qrcode.version}')
	print(f'  alignments: {qrcode.alignment_count}')
	print(f'         ecl: {qrcode.ecl}')
	print(f' data format: {qrcode.data_format}')
	print(f'  bit length: {qrcode.bit_length}')
	print(f'   bit count: {qrcode.total_bit_count}')

	# qrcode.preview()
	# qrcode.export_bmp('preview')
